(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
在過去幾天,我們都是用map來製造MenuItem。但如果你眼尖的話,應該會注意到在console噴出了這個東西
照字面翻的話就是React希望我們給MenuItem一個叫做key的props,且每個MenuItem的key最好都是不一樣的。
為什麼需要這個props呢?
現在我們先把前面的Menu系列的元件改造一下:
useContext
也就是程式碼長這樣:
import React, {useState,useMemo} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];
const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    const [menuItemData, setMenuItemData] = useState(menuItemWording);
    let menuItemArr = useMemo(()=> 
        menuItemData.map(
            (wording) => <MenuItem text={wording}/>
        ),[menuItemData]);
    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                let menuDataCopy = ["測試資料"].concat(menuItemData);
                setMenuItemData(menuDataCopy); 
            }}>更改第一個menuItem</button>
        </OpenContext.Provider>
    );
}
import React, {memo} from 'react';
const menuItemStyle = {
    marginBottom: "7px",
    paddingLeft: "26px",
    listStyle: "none"
};
function MenuItem(props){
    return  <li style={menuItemStyle}>{props.text}</li>;
}
export default memo(MenuItem);
接著開啟dev tool的Profile後,按下這個新增用的按鍵,接著你會看到:

為什麼會這樣呢?
這是因為當陣列元素的索引位置被改變時,React會認為其製造出來的陣列元素也被改變。這裡我們的元素都各往後移動了一個索引位置,所以React就把元素重新渲染了。
key是React用來辨識陣列元件、決定是否要重新渲染的工具。當陣列元件被改變,React會去比較「同key值的元件」和上次渲染時的值一不一樣,不一樣的時候才會重新渲染該元件。以下表為例,因為a、b、c、d對應到的props都和前一次一樣,所以React不會重新渲染他們。
| 舊(id) | 新(id) | 
|---|---|
| A( a ) | Z( z ) | 
| B( b ) | A( a ) | 
| C( c ) | B( b ) | 
| D( d ) | C( c ) | 
| D( d ) | |
| 這也是為什麼剛剛React會希望我們綁一個 key在MenuItem上。 | 
同時,我們也不應該拿元件在陣列中的索引值當作key,因為以剛剛的例子來說,每個元件的索引值都往後1了。所以雖然除了在開頭新增了一個元件外,其他元件都沒有被改變,不應該被重新渲染,但如果你使用了索引值當作key,相同key對應到的內容就不一樣了,那等於沒有放key的狀況。
以下方為例,1對應到的元件props從B變成A,所以React會重新渲染他,以此類推A~D都會重新渲染。
| 舊(id) | 新(id) | 
|---|---|
| A( 0 ) | Z( 0 ) | 
| B( 1 ) | A( 1 ) | 
| C( 2 ) | B( 2 ) | 
| D( 3 ) | C( 3 ) | 
| D( 4 ) | 
因為當單一wording被改變時,對應到的該單一元件本來就應該要重新渲染,所以在這個case我們就能拿來當key的值。實際上最好應該要有一個uuid之類的東西。
現在,我們如果把wording當成key綁在MenuItem上
import React, {useState,useMemo} from 'react';
import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';
let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];
const MenuPage = () =>{
    const [isOpen, setIsOpen] = useState(true);
    const [menuItemData, setMenuItemData] = useState(menuItemWording);
    let menuItemArr = useMemo(()=>
            menuItemData.map((wording) => 
                <MenuItem 
                    text={wording} 
                    key={wording}
                />
            ),[menuItemData]);
    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: setIsOpen
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                let menuDataCopy = ["測試資料"].concat(menuItemData);
                setMenuItemData(menuDataCopy); 
            }}>更改第一個menuItem</button>
        </OpenContext.Provider>
    );
}
export default MenuPage;
重新執行並監聽效能,你就會發現只有新進來的MenuItem被重新渲染了。

官方資料
為什麼不要總是直接用 array 的 index 當 React Component 的 Key